/******************************************************************************
* Copyright (C) Ultraleap, Inc. 2011-2021. *
* *
* Use subject to the terms of the Apache License 2.0 available at *
* http://www.apache.org/licenses/LICENSE-2.0, or another agreement *
* between Ultraleap and you, your company or other organization. *
******************************************************************************/
using Leap.Unity.Attributes;
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
namespace Leap.Unity.Interaction
{
///
/// A physics-enabled slider. Sliding is triggered by physically pushing the slider to its compressed position.
/// Increasing the horizontal and vertical slide limits allows it to act as either a 1D or 2D slider.
///
public class InteractionSlider : InteractionButton
{
#region Inspector & Properties
public enum SliderType
{
Vertical,
Horizontal,
TwoDimensional
}
[Header("Slider Settings")]
public SliderType sliderType = SliderType.Horizontal;
public bool dispatchSlideValueOnStart = true;
[Tooltip("Manually specify slider limits even if the slider's parent has a RectTransform.")]
[DisableIf("_parentHasRectTransform", isEqualTo: false)]
public bool overrideRectLimits = false;
[SerializeField, HideInInspector]
#pragma warning disable 0414
private bool _parentHasRectTransform = false;
#pragma warning restore 0414
[Header("Horizontal Axis")]
public float defaultHorizontalValue;
[Tooltip("The minimum and maximum values that the slider reports on the horizontal axis.")]
[FormerlySerializedAs("horizontalValueRange")]
[SerializeField]
private Vector2 _horizontalValueRange = new Vector2(0f, 1f);
public float minHorizontalValue
{
get
{
return _horizontalValueRange.x;
}
set
{
if (value != _horizontalValueRange.x)
{
_horizontalValueRange.x = value;
HorizontalSlideEvent(HorizontalSliderValue);
}
}
}
public float maxHorizontalValue
{
get
{
return _horizontalValueRange.y;
}
set
{
if (value != _horizontalValueRange.y)
{
_horizontalValueRange.y = value;
HorizontalSlideEvent(HorizontalSliderValue);
}
}
}
[Tooltip("The minimum and maximum horizontal extents that the slider can slide to in world space.")]
[MinMax(-0.5f, 0.5f)]
public Vector2 horizontalSlideLimits = new Vector2(-0.05f, 0.05f);
[Tooltip("The number of discrete quantized notches **beyond the first** that this "
+ "slider can occupy on the horizontal axis. A value of zero indicates a "
+ "continuous (non-quantized) slider for this axis.")]
[MinValue(0)]
public int horizontalSteps = 0;
[System.Serializable]
public class FloatEvent : UnityEvent { }
/// Triggered while this slider is depressed.
[SerializeField]
[FormerlySerializedAs("horizontalSlideEvent")]
private FloatEvent _horizontalSlideEvent = new FloatEvent();
[Header("Vertical Axis")]
public float defaultVerticalValue;
[Tooltip("The minimum and maximum values that the slider reports on the horizontal axis.")]
[FormerlySerializedAs("verticalValueRange")]
[SerializeField]
private Vector2 _verticalValueRange = new Vector2(0f, 1f);
public float minVerticalValue
{
get
{
return _verticalValueRange.x;
}
set
{
if (value != _verticalValueRange.x)
{
_verticalValueRange.x = value;
VerticalSlideEvent(VerticalSliderValue);
}
}
}
public float maxVerticalValue
{
get
{
return _verticalValueRange.y;
}
set
{
if (value != _verticalValueRange.y)
{
_verticalValueRange.y = value;
VerticalSlideEvent(VerticalSliderValue);
}
}
}
[MinMax(-0.5f, 0.5f)]
[Tooltip("The minimum and maximum vertical extents that the slider can slide to in world space.")]
public Vector2 verticalSlideLimits = new Vector2(0f, 0f);
[Tooltip("The number of discrete quantized notches **beyond the first** that this "
+ "slider can occupy on the vertical axis. A value of zero indicates a "
+ "continuous (non-quantized) slider for this axis.")]
[MinValue(0)]
public int verticalSteps = 0;
/// Triggered while this slider is depressed.
[SerializeField]
[FormerlySerializedAs("verticalSlideEvent")]
private FloatEvent _verticalSlideEvent = new FloatEvent();
public Action HorizontalSlideEvent = (f) => { };
public Action VerticalSlideEvent = (f) => { };
public float HorizontalSliderPercent
{
get
{
return _horizontalSliderPercent;
}
set
{
if (!_started)
{
Debug.LogWarning("An object is attempting to access this slider's value before it has been initialized! Initializing now; this could yield unexpected behaviour...", this);
Start();
}
_horizontalSliderPercent = value;
localPhysicsPosition.x = Mathf.Lerp(initialLocalPosition.x + horizontalSlideLimits.x, initialLocalPosition.x + horizontalSlideLimits.y, _horizontalSliderPercent);
physicsPosition = transform.parent.TransformPoint(localPhysicsPosition);
rigidbody.position = physicsPosition;
}
}
public float VerticalSliderPercent
{
get
{
return _verticalSliderPercent;
}
set
{
if (!_started)
{
Debug.LogWarning("An object is attempting to access this slider's value before it has been initialized! Initializing now; this could yield unpected behaviour...", this);
Start();
}
_verticalSliderPercent = value;
localPhysicsPosition.y = Mathf.Lerp(initialLocalPosition.y + verticalSlideLimits.x, initialLocalPosition.y + verticalSlideLimits.y, _verticalSliderPercent);
physicsPosition = transform.parent.TransformPoint(localPhysicsPosition);
rigidbody.position = physicsPosition;
}
}
/// This slider's horizontal slider value, mapped between the values in the HorizontalValueRange.
public float HorizontalSliderValue
{
get
{
return Mathf.Lerp(_horizontalValueRange.x, _horizontalValueRange.y, _horizontalSliderPercent);
}
set
{
HorizontalSliderPercent = Mathf.InverseLerp(_horizontalValueRange.x, _horizontalValueRange.y, value);
}
}
/// This slider's current vertical slider value, mapped between the values in the VerticalValueRange.
public float VerticalSliderValue
{
get
{
return Mathf.Lerp(_verticalValueRange.x, _verticalValueRange.y, _verticalSliderPercent);
}
set
{
VerticalSliderPercent = Mathf.InverseLerp(_verticalValueRange.x, _verticalValueRange.y, value);
}
}
private void calculateSliderValues()
{
// Calculate renormalized slider values.
if (horizontalSlideLimits.x != horizontalSlideLimits.y)
{
_horizontalSliderPercent = Mathf.InverseLerp(initialLocalPosition.x + horizontalSlideLimits.x, initialLocalPosition.x + horizontalSlideLimits.y, localPhysicsPosition.x);
HorizontalSlideEvent(HorizontalSliderValue);
}
if (verticalSlideLimits.x != verticalSlideLimits.y)
{
_verticalSliderPercent = Mathf.InverseLerp(initialLocalPosition.y + verticalSlideLimits.x, initialLocalPosition.y + verticalSlideLimits.y, localPhysicsPosition.y);
VerticalSlideEvent(VerticalSliderValue);
}
}
public float normalizedHorizontalValue
{
get
{
return _horizontalSliderPercent;
}
set
{
var newValue = Mathf.Clamp01(value);
if (newValue != _horizontalSliderPercent)
{
_horizontalSliderPercent = newValue;
HorizontalSlideEvent(HorizontalSliderValue);
}
}
}
public float normalizedVerticalValue
{
get
{
return _verticalSliderPercent;
}
set
{
var newValue = Mathf.Clamp01(value);
if (newValue != _verticalSliderPercent)
{
_verticalSliderPercent = newValue;
VerticalSlideEvent(VerticalSliderValue);
}
}
}
///
/// Returns the number of horizontal steps past the minimum value of the slider, for
/// sliders with a non-zero number of horizontal steps. This value is independent of
/// the horizontal value range of the slider. For example a slider with a
/// horizontalSteps value of 9 could have horizontalStepValues of 0-9.
///
public int horizontalStepValue
{
get
{
float range = _horizontalValueRange.y - _horizontalValueRange.x;
if (range == 0F) return 0;
else
{
return (int)(_horizontalSliderPercent * horizontalSteps * 1.001F);
}
}
}
///
/// Gets whether the slider was slide in the latest Update().
///
public bool wasSlid
{
get { return _wasSlid && _sawWasSlid; }
}
#endregion
#region Internal State
protected float _horizontalSliderPercent;
protected float _verticalSliderPercent;
protected RectTransform parent;
private bool _started = false;
private bool _sawWasSlid = false;
private bool _wasSlid = false;
#endregion
#region Unity Events
protected override void OnValidate()
{
base.OnValidate();
if (this.transform.parent != null && this.transform.parent.GetComponent() != null)
{
_parentHasRectTransform = true;
}
else
{
_parentHasRectTransform = false;
}
}
protected override void OnEnable()
{
base.OnEnable();
OnContactStay += calculateSliderValues;
}
protected override void OnDisable()
{
OnContactStay -= calculateSliderValues;
base.OnDisable();
}
protected override void Start()
{
if (_started) return;
_started = true;
calculateSliderLimits();
switch (sliderType)
{
case SliderType.Horizontal:
verticalSlideLimits = new Vector2(0, 0);
break;
case SliderType.Vertical:
horizontalSlideLimits = new Vector2(0, 0);
break;
}
base.Start();
HorizontalSlideEvent += _horizontalSlideEvent.Invoke;
VerticalSlideEvent += _verticalSlideEvent.Invoke;
HorizontalSlideEvent += (f) => { _wasSlid = true; };
VerticalSlideEvent += (f) => { _wasSlid = true; };
HorizontalSliderValue = defaultHorizontalValue;
VerticalSliderValue = defaultVerticalValue;
if (dispatchSlideValueOnStart)
{
HorizontalSlideEvent(HorizontalSliderValue);
VerticalSlideEvent(VerticalSliderValue);
}
}
protected override void Update()
{
base.Update();
if (!Application.isPlaying) { return; }
if (isPressed || isGrasped)
{
calculateSliderValues();
}
// Whenever "_wasSlid" is set to true (however many times between Update() cycles),
// this logic produces a single Update() cycle through which slider.wasSlid
// will return true. (This allows observing MonoBehaviours to simply check
// "wasSlid" during their Update() cycle to perform updating logic.)
if (_wasSlid && !_sawWasSlid)
{
_sawWasSlid = true;
}
else if (_sawWasSlid)
{
_wasSlid = false;
_sawWasSlid = false;
}
}
#endregion
#region Public API
public void RecalculateSliderLimits()
{
calculateSliderLimits();
}
#endregion
#region Internal Methods
private void calculateSliderLimits()
{
if (transform.parent != null)
{
parent = transform.parent.GetComponent();
if (overrideRectLimits) return;
if (parent != null)
{
if (parent.rect.width < 0f || parent.rect.height < 0f)
{
Debug.LogError("Parent Rectangle dimensions negative; can't set slider boundaries!", parent.gameObject);
enabled = false;
}
else
{
var self = transform.GetComponent();
if (self != null)
{
horizontalSlideLimits = new Vector2(parent.rect.xMin - transform.localPosition.x + self.rect.width / 2F, parent.rect.xMax - transform.localPosition.x - self.rect.width / 2F);
if (horizontalSlideLimits.x > horizontalSlideLimits.y)
{
horizontalSlideLimits = new Vector2(0F, 0F);
}
if (Mathf.Abs(horizontalSlideLimits.x) < 0.0001F)
{
horizontalSlideLimits.x = 0F;
}
if (Mathf.Abs(horizontalSlideLimits.y) < 0.0001F)
{
horizontalSlideLimits.y = 0F;
}
verticalSlideLimits = new Vector2(parent.rect.yMin - transform.localPosition.y + self.rect.height / 2F, parent.rect.yMax - transform.localPosition.y - self.rect.height / 2F);
if (verticalSlideLimits.x > verticalSlideLimits.y)
{
verticalSlideLimits = new Vector2(0F, 0F);
}
if (Mathf.Abs(verticalSlideLimits.x) < 0.0001F)
{
verticalSlideLimits.x = 0F;
}
if (Mathf.Abs(verticalSlideLimits.y) < 0.0001F)
{
verticalSlideLimits.y = 0F;
}
}
else
{
horizontalSlideLimits = new Vector2(parent.rect.xMin - transform.localPosition.x, parent.rect.xMax - transform.localPosition.x);
verticalSlideLimits = new Vector2(parent.rect.yMin - transform.localPosition.y, parent.rect.yMax - transform.localPosition.y);
}
}
}
}
}
protected override Vector3 constrainDepressedLocalPosition(Vector3 desiredOffset)
{
Vector3 unSnappedPosition =
new Vector3(Mathf.Clamp((localPhysicsPosition.x + desiredOffset.x), initialLocalPosition.x + horizontalSlideLimits.x, initialLocalPosition.x + horizontalSlideLimits.y),
Mathf.Clamp((localPhysicsPosition.y + desiredOffset.y), initialLocalPosition.y + verticalSlideLimits.x, initialLocalPosition.y + verticalSlideLimits.y),
(localPhysicsPosition.z + desiredOffset.z));
float hSliderPercent = Mathf.InverseLerp(initialLocalPosition.x + horizontalSlideLimits.x, initialLocalPosition.x + horizontalSlideLimits.y, unSnappedPosition.x);
if (horizontalSteps > 0)
{
hSliderPercent = Mathf.Round(hSliderPercent * (horizontalSteps)) / (horizontalSteps);
}
float vSliderPercent = Mathf.InverseLerp(initialLocalPosition.y + verticalSlideLimits.x, initialLocalPosition.y + verticalSlideLimits.y, unSnappedPosition.y);
if (verticalSteps > 0)
{
vSliderPercent = Mathf.Round(vSliderPercent * (verticalSteps)) / (verticalSteps);
}
return new Vector3(Mathf.Lerp(initialLocalPosition.x + horizontalSlideLimits.x, initialLocalPosition.x + horizontalSlideLimits.y, hSliderPercent),
Mathf.Lerp(initialLocalPosition.y + verticalSlideLimits.x, initialLocalPosition.y + verticalSlideLimits.y, vSliderPercent),
(localPhysicsPosition.z + desiredOffset.z));
}
#endregion
#region Gizmos
protected override void OnDrawGizmosSelected()
{
base.OnDrawGizmosSelected();
if (transform.parent != null)
{
Vector3 originPosition = Application.isPlaying ? initialLocalPosition : transform.localPosition;
if (Application.isPlaying && startingPositionMode == StartingPositionMode.Relaxed)
{
originPosition = originPosition + Vector3.back * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, restingHeight);
}
// Actual slider slide limits
Gizmos.color = Color.blue;
Gizmos.DrawWireCube(originPosition +
new Vector3((sliderType == SliderType.Vertical ? 0F : (horizontalSlideLimits.x + horizontalSlideLimits.y) * 0.5f),
(sliderType == SliderType.Horizontal ? 0F : (verticalSlideLimits.x + verticalSlideLimits.y) * 0.5f),
0f),
new Vector3((sliderType == SliderType.Vertical ? 0F : horizontalSlideLimits.y - horizontalSlideLimits.x),
(sliderType == SliderType.Horizontal ? 0F : verticalSlideLimits.y - verticalSlideLimits.x),
0f));
var self = GetComponent();
if (self != null)
{
// Apparent slide limits (against own rect limits)
parent = transform.parent.GetComponent();
if (parent != null)
{
var parentRectHorizontal = new Vector2(parent.rect.xMin - originPosition.x, parent.rect.xMax - originPosition.x);
var parentRectVertical = new Vector2(parent.rect.yMin - originPosition.y, parent.rect.yMax - originPosition.y);
Gizmos.color = Color.Lerp(Color.blue, Color.cyan, 0.5F);
Gizmos.DrawWireCube(originPosition +
new Vector3((parentRectHorizontal.x + parentRectHorizontal.y) * 0.5f,
(parentRectVertical.x + parentRectVertical.y) * 0.5f,
(startingPositionMode == StartingPositionMode.Relaxed ?
Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, 0.5F) - Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, 1 - restingHeight)
: -1F * Mathf.Lerp(minMaxHeight.x, minMaxHeight.y, 0.5F))),
new Vector3(parentRectHorizontal.y - parentRectHorizontal.x, parentRectVertical.y - parentRectVertical.x, (minMaxHeight.y - minMaxHeight.x)));
// Own rect width/height
Gizmos.color = Color.cyan;
Gizmos.DrawWireCube(originPosition,
self.rect.width * Vector3.right
+ self.rect.height * Vector3.up);
}
}
}
}
#endregion
}
}